Skip to content

Principle of Least Privilege

Video Summary

Let's talk about one more thing about the “shopping list” example from Module 2:

Screenshot of the “shopping list” app, showing 3 items on the list: apple, banana, carrot

In my solution, I created a handleAddItem function, and passed it down to the child component:

function App() {
const [items, setItems] = React.useState([]);
function handleAddItem(label) {
const newItem = {
label,
id: Math.random(),
};
const nextItems = [...items, newItem];
setItems(nextItems);
}
return (
<div className="wrapper">
<div className="list-wrapper">
...
</div>
<AddNewItemForm
handleAddItem={handleAddItem}
/>
</div>
);
}

Many students have wondered why I've structured things this way. Wouldn't it be simpler to pass the state-setter function directly?

function App() {
const [items, setItems] = React.useState([]);
return (
<div className="wrapper">
<div className="list-wrapper">
...
</div>
<AddNewItemForm
setItems={setItems}
/>
</div>
);
}

This approach works, but there's a reason that I structured things the way I did. It has to do with one of the most important mental models I've learned, when it comes to working with React: the principle of least privilege.

This term comes from the security field, and it has to do with reducing the amount of access/authorization every member of an organization has.

Let's suppose I work as a bank teller. For me to do my job, I need the authorization to do things like open accounts for new customers, or handle deposits. But I probably shouldn't be able to issue mortgages, or buy/sell stock options. Those things fall outside the scope of my role, and so the computer system shouldn't authorize me to perform those actions.

Now, let's imagine that every component in our app is a member of our organization.

The AddNewItemForm component has one job: it needs to be able to push a new item to the end of the current list of items.

When we give it a state-setter function, we grant it so much more power than that. For example, it can erase all of the current items:

// Erases all previously-saved items:
setItems([]);

Or, it can break the app altogether, by setting it to something other than an array:

setItems(5);

By contrast, in my original solution, the AddNewItemForm component can't do any of that stuff. The only thing it can do is push a new item into the array:

function AddNewItemForm({ handleAddItem }) {
const [label, setLabel] = React.useState('');
return (
<div className="new-list-item-form">
<form
onSubmit={(event) => {
event.preventDefault();
handleAddItem(label);
setLabel('')
}}
>
...
</form>
</div>
);
}

This component can still break things (eg. by calling handleAddItem with a random number instead of label), but it has far less power. It can't cause as many problems.

Now, you might think this whole concept sounds a bit strange. Components aren't sentient! Either way, it's us developers who are deciding what to pass to setItems. We wouldn't maliciously pass it a number! Why does it matter where we call setItems?

This is one of those things that really only makes sense at scale. This example has less than 150 lines of code in the entire application. But what if we were working on a codebase with hundreds of thousands of lines of code? If we were 1 of 50 developers on the project?

In real-world production settings like this, we're often tossed into corners of the codebase that we aren't familiar with at all. You might be tasked with tweaking the AddNewItemForm component without knowing anything about how it works!

In those types of situations, it's so so easy to introduce subtle bugs. And so, the less privilege we give to our components, the less problems we'll have down the road.

Here's the original solution, with the handleAddItems handler function:

My Original Solution

import React from 'react';

import AddNewItemForm from './AddNewItemForm';

function App() {
const [items, setItems] = React.useState([]);
function handleAddItem(label) {
const newItem = {
label,
id: Math.random(),
};

const nextItems = [...items, newItem];
setItems(nextItems);
}

return (
<div className="wrapper">
<div className="list-wrapper">
<ol className="shopping-list">
{items.map(({ id, label }) => (
<li key={id}>{label}</li>
))}
</ol>
</div>
<AddNewItemForm
handleAddItem={handleAddItem}
/>
</div>
);
}

export default App;

And here's the not-recommended alternative, where we pass the state-setter function directly:

Passing the setter function

import React from 'react';

import AddNewItemForm from './AddNewItemForm';

function App() {
const [items, setItems] = React.useState([]);
return (
<div className="wrapper">
<div className="list-wrapper">
<ol className="shopping-list">
{items.map(({ id, label }) => (
<li key={id}>{label}</li>
))}
</ol>
</div>
<AddNewItemForm
setItems={setItems}
/>
</div>
);
}

export default App;